Skip to content

Distance fog: height falloff, so mist pools in low ground - #1634

Merged
obiot merged 1 commit into
masterfrom
feat/fog-height-falloff
Sep 1, 2026
Merged

Distance fog: height falloff, so mist pools in low ground#1634
obiot merged 1 commit into
masterfrom
feat/fog-height-falloff

Conversation

@obiot

@obiot obiot commented Sep 1, 2026

Copy link
Copy Markdown
Member

Closes #1633. Builds on #1622.

The problem

Uniform fog is one dial doing two jobs. Density is the same everywhere, so only the distance through it varies — a ridge top 3000 units away fogs exactly as much as the valley floor 3000 units away. Tune it so a valley has atmosphere and the skyline washes out with it; tune it so the peaks stay crisp and the low ground has no air in it at all.

The change

camera.setFog({ near: 1200, far: 7000, fogHeight: 0, heightFalloff: 0.0015 });

Density drops exponentially with altitude, so mist fills the hollows and thins over the ridges. That is the difference between fog reading as weather and reading as a global desaturation.

It multiplies the distance rather than adding a third curve, so "linear" and "exp2" are untouched and both gain it for free. An exponential integrates analytically along a straight segment, so the whole ray costs one exp in the vertex stage — no marching, no volume texture.

Additive, and provably so

heightFalloff defaults to 0, and that is not a special case: kdy is zero, the series limit is taken, exp(0) is one, and the factor is exactly 1. The first test asserts a zero-falloff frame is byte for byte the frame without the feature.

The three traps the issue predicted

outcome
Y-down handled. Every published form of this assumes Y-up, so density rises as y increases here. Flipping it puts mist on the peaks instead of in the valley — mutation-checked, one test fails
The horizontal ray handled. (exp(x) − 1) / x is 0/0 when the ray is level, which is the common case looking across a valley. The series limit is taken rather than a guard, or fog would step as the ray flattened — mutation-checked, seven tests fail
Overflow below the reference height clamped, and the test says honestly that it does not prove the clamp: with or without it the result saturates, because the fog curve clamps an infinite distance anyway. It is insurance against a driver carrying Inf into an interpolated varying, which this cannot observe

Notes

  • The WebGPU uniform block grows 240 → 256. Three specs pin that size and are re-pinned deliberately.
  • Applied in the vertex stage, folded into the existing distance varying, so the fragment code is unchanged and the two tiers stay consistent.

Verification

270 files / 6531 tests, lint and types clean. Checked on screen on both backends — WebGL, and WebGPU on a real Metal adapter: mist filling the valley run while the wooded walls stay crisp, and the unfogged examples (Instanced Forest, glTF, AfterBurner) unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N

Uniform fog gives one dial for two jobs. Tune it so a valley has
atmosphere and the skyline washes out with it; tune it so the peaks stay
crisp and the low ground has no air in it. The fog cannot tell which is
high and which is low, because density is the same everywhere and only
the distance through it varies.

`heightFalloff` adds the second falloff. Density drops exponentially with
altitude, so mist fills the hollows and thins over the ridges.

It multiplies the DISTANCE rather than adding a third curve, so "linear"
and "exp2" are untouched and both gain it for free. An exponential
integrates analytically along a straight segment, so the whole ray costs
one `exp` in the vertex stage — no marching, no volume texture.

`heightFalloff` defaults to 0, and that is not a special case: `kdy` is
zero, the series limit is taken, `exp(0)` is one, and the factor is
exactly 1. A scene that omits it renders identically, which the first
test asserts byte for byte.

Three things the issue predicted would bite, all handled and two of them
mutation-checked:

  - Y-DOWN. Every published form assumes Y-up, so density rises as `y`
    INCREASES here. Flipping it puts the mist on the peaks instead of in
    the valley, and the test catches it.
  - The horizontal ray. `(exp(x) - 1) / x` is 0/0 when the view ray is
    level, which is the common case looking across a valley. The series
    limit is taken rather than a guard, or the fog would step as the ray
    flattened; removing it fails seven tests.
  - Overflow with the camera far below the reference height. Clamped.
    Noted honestly in the test: this one is NOT observable through the
    fog curve, which clamps an infinite distance anyway. It is insurance
    against a driver carrying `Inf` into an interpolated varying.

The WebGPU uniform block grows 240 -> 256; the three specs that pin its
size are re-pinned deliberately.

Verified on screen on both backends: mist filling the run while the
wooded walls stay crisp, and the unfogged examples unchanged.

Closes #1633

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N
Copilot AI lite review requested due to automatic review settings September 1, 2026 01:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends melonJS’s existing 3D distance fog to support height-based density falloff, enabling “mist pooling” in low ground while keeping higher terrain/crisp skylines less fogged. It does this by multiplying the existing fog distance term by an analytically integrated height factor, keeping the existing "linear" and "exp2" fog curves intact.

Changes:

  • Adds fogHeight and heightFalloff options/state to Camera3d.setFog() and the fog state passed to renderers/shaders.
  • Extends WebGL + WebGPU mesh vertex shaders to scale vFogDepth by a height-integral factor (fragment fog code stays unchanged).
  • Grows the WebGPU MeshUniforms block from 240 → 256 bytes to carry the additional fog-height parameters, and updates/expands tests accordingly.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/melonjs/tests/webgpu_mtl_material.spec.js Updates WebGPU mesh uniform block size expectation to 256 bytes.
packages/melonjs/tests/webgpu_mesh_fog.spec.js Updates WebGPU fog uniform block sizing/layout-key tests; adds new height-falloff-related defaults in fixtures.
packages/melonjs/tests/webgpu_mesh_batcher.spec.js Updates WebGPU pipeline layout key and uniform size expectations to mesh:u256 / 256 bytes.
packages/melonjs/tests/webgl_mesh_fog.spec.js Adds WebGL integration tests covering zero-falloff equivalence, Y-down sign, horizontal-ray degeneracy, and extreme camera height behavior.
packages/melonjs/src/video/webgpu/shaders/mesh.wgsl Adds fogHeight uniform vec4 and applies height factor to fog depth in unlit mesh WGSL.
packages/melonjs/src/video/webgpu/shaders/mesh-shadow-instanced.wgsl Applies the same height-based fog depth scaling to instanced shadow blobs for backend consistency.
packages/melonjs/src/video/webgpu/shaders/mesh-lit.wgsl Adds fogHeight uniform vec4 and applies height factor to fog depth in lit mesh WGSL.
packages/melonjs/src/video/webgpu/shaders/mesh-instanced.js Updates generated instanced WGSL vertex bodies to include height factor when writing fog depth.
packages/melonjs/src/video/webgpu/batchers/mesh_batcher.js Expands the per-draw uniform packing to 256 bytes and writes the new fog-height parameters at floats 60–63.
packages/melonjs/src/video/webgl/shaders/mesh.vert Adds uFogHeight and scales fog depth by fogHeightFactor in the unlit WebGL vertex shader.
packages/melonjs/src/video/webgl/shaders/mesh-shadow-instanced.vert Adds uFogHeight and scales fog depth similarly for instanced shadow rendering.
packages/melonjs/src/video/webgl/shaders/mesh-lit.vert Adds uFogHeight and scales fog depth for the lit WebGL vertex shader.
packages/melonjs/src/video/webgl/shaders/mesh-lit-instanced.vert Adds uFogHeight and scales fog depth for lit instanced meshes.
packages/melonjs/src/video/webgl/shaders/mesh-instanced.vert Adds uFogHeight and scales fog depth for unlit instanced meshes.
packages/melonjs/src/video/webgl/batchers/mesh_batcher.js Adds uFogHeight uniform upload/caching for WebGL mesh draws.
packages/melonjs/src/camera/fog.ts Extends fog option/state types with fogHeight, heightFalloff, and cameraY.
packages/melonjs/src/camera/camera3d.ts Validates/stores new fog options and resolves per-frame fog state including camera Y for shader integration.
packages/melonjs/skills/melonjs-3d/SKILL.md Updates 3D skill docs/examples to include height falloff usage and Y-down sign caveats.
packages/melonjs/CHANGELOG.md Adds release note entry describing the new height falloff fog feature and its behavior.
Suppressed comments (1)

packages/melonjs/tests/webgpu_mesh_fog.spec.js:105

  • The uniform-layout test verifies fogColor (52-54) and fogParams (56-59), but doesn't assert the new fogHeight vec4 (60-63). Adding an assertion here would pin the new offsets and ensure the new fields are zeroed/written correctly.
		it("writes the fog colour at 52-54 and the params at 56-59", () => {
			install(fog());
			batcher.drawRetainedMesh(makeMesh(), MODEL, 0xffffffff);
			const floats = snapshot();
			// model 0-15, view 16-31, tint 32-35, params 36-39, emissive 40-43,

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +39 to +42
// Density falls off exponentially with altitude, and an exponential integrates
// analytically along a straight segment, so the whole ray costs one `exp` and
// no marching. The result multiplies the distance, which leaves both fog
// curves exactly as they are.
@obiot
obiot merged commit 10b04cf into master Sep 1, 2026
7 checks passed
@obiot
obiot deleted the feat/fog-height-falloff branch September 1, 2026 02:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Distance fog: height falloff, so mist pools in low ground

2 participants